package com.ceair.authorization.device;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;

/**
 * @author wangbaohai
 * @ClassName DeviceClientAuthenticationProvider
 * @description: 设备码认证提供者
 * @date 2024年11月15日
 * @version: 1.0.0
 */
@Slf4j
@RequiredArgsConstructor
public final class DeviceClientAuthenticationProvider implements AuthenticationProvider {

    /**
     * 异常说明地址
     */
    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
    private final RegisteredClientRepository registeredClientRepository;

    /**
     * 当客户端认证失败时抛出异常
     *
     * @param parameterName 认证失败的参数名称，用于在错误消息中指明具体参数
     * @throws OAuth2AuthenticationException 每当认证失败时抛出此异常，包含错误代码和描述信息
     */
    private static void throwInvalidClient(String parameterName) {
        // 创建一个OAuth2Error实例，用于描述客户端认证失败的错误详情
        OAuth2Error error = new OAuth2Error(
                OAuth2ErrorCodes.INVALID_CLIENT,
                "Device client authentication failed: " + parameterName,
                ERROR_URI
        );
        // 抛出OAuth2AuthenticationException异常，用于处理客户端认证失败的情况
        throw new OAuth2AuthenticationException(error);
    }

    /**
     * 认证设备客户端
     *
     * 此方法专用于处理设备码流程的认证请求它验证客户端是否为已注册的公共客户端，
     * 并根据客户端的认证方法和注册信息来确定是否授权该请求
     *
     * @param authentication 认证对象，包含客户端的认证信息
     * @return 如果认证成功，返回新的认证令牌；如果认证失败或不支持的认证方法，返回null
     * @throws AuthenticationException 如果认证过程中发生任何错误，抛出此异常
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 执行时肯定是设备码流程
        DeviceClientAuthenticationToken deviceClientAuthentication =
                (DeviceClientAuthenticationToken) authentication;

        // 只支持公共客户端
        if (!ClientAuthenticationMethod.NONE.equals(deviceClientAuthentication.getClientAuthenticationMethod())) {
            return null;
        }

        // 获取客户端id并查询
        String clientId = deviceClientAuthentication.getPrincipal().toString();
        RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
        if (registeredClient == null) {
            throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
        }

        if (log.isTraceEnabled()) {
            log.trace("Retrieved registered client");
        }

        // 校验客户端
        if (!registeredClient.getClientAuthenticationMethods().contains(
                deviceClientAuthentication.getClientAuthenticationMethod())) {
            throwInvalidClient("authentication_method");
        }

        if (log.isTraceEnabled()) {
            log.trace("Validated device client authentication parameters");
        }

        if (log.isTraceEnabled()) {
            log.trace("Authenticated device client");
        }

        // 返回新的认证令牌
        return new DeviceClientAuthenticationToken(registeredClient,
                deviceClientAuthentication.getClientAuthenticationMethod(), null);
    }

    /**
     * 判断此认证器支持的认证类型
     *
     * @param authentication 认证类的类型
     * @return 如果认证类是DeviceClientAuthenticationToken类型或其子类，则返回true，否则返回false
     */
    @Override
    public boolean supports(Class<?> authentication) {
        // 只处理设备码请求
        return DeviceClientAuthenticationToken.class.isAssignableFrom(authentication);
    }

}
